/**
 * @author Ed Spencer (http://sencha.com) Transition plugin for DataViews
 */
Ext.define('Ext.ux.DataView.Animated', {

	/**
	 * @property defaults
	 * @type Object Default configuration options for all DataViewTransition
	 *       instances
	 */
	defaults : {
		duration : 750,
		idProperty : 'id'
	},

	/**
	 * Creates the plugin instance, applies defaults
	 * 
	 * @constructor
	 * @param {Object}
	 *            config Optional config object
	 */
	constructor : function(config) {
		Ext.apply(this, config || {}, this.defaults);
	},

	/**
	 * Initializes the transition plugin. Overrides the dataview's default
	 * refresh function
	 * 
	 * @param {Ext.view.View}
	 *            dataview The dataview
	 */
	init : function(dataview) {
		/**
		 * @property dataview
		 * @type Ext.view.View Reference to the DataView this instance is bound
		 *       to
		 */
		this.dataview = dataview;

		var store = dataview.store;

		dataview.blockRefresh = true;
		dataview.updateIndexes = Ext.Function.createSequence(
				dataview.updateIndexes, function() {
					this.getTargetEl().select(this.itemSelector).each(
							function(element, composite, index) {
								element.id = element.dom.id = Ext.util.Format
										.format("{0}-{1}", dataview.id,
												store.getAt(index).internalId);
							}, this);
				}, dataview);

		/**
		 * @property dataviewID
		 * @type String The string ID of the DataView component. This is used
		 *       internally when animating child objects
		 */
		this.dataviewID = dataview.id;

		/**
		 * @property cachedStoreData
		 * @type Object A cache of existing store data, keyed by id. This is
		 *       used to determine whether any items were added or removed from
		 *       the store on data change
		 */
		this.cachedStoreData = {};

		// catch the store data with the snapshot immediately
		this.cacheStoreData(store.data || store.snapshot);

		dataview.on('resize', function() {
					var store = dataview.store;
					if (store.getCount() > 0) {
						// reDraw.call(this, store);
					}
				}, this);

		dataview.store.on('datachanged', reDraw, this);

		function reDraw(store) {
			var parentEl = dataview.getTargetEl(), calcItem = store.getAt(0), added = this
					.getAdded(store), removed = this.getRemoved(store), previous = this
					.getRemaining(store);

			// Not yet rendered
			if (!parentEl) {
				return;
			}

			// make sure the correct styles are applied to the parent element
			if (Ext.isIEQuirks) {
				parentEl.applyStyles({
							zoom : 1,
							display : 'block',
							position : 'relative'
						});
			}

			// hide old items
			Ext.each(removed, function(item) {
						var id = this.dataviewID + '-' + item.internalId;
						Ext.fly(id).animate({
									remove : false,
									duration : duration,
									opacity : 0,
									useDisplay : true,
									callback : function() {
										Ext.fly(id).setDisplayed(false);
									}
								});
					}, this);

			// store is empty
			if (calcItem == undefined) {
				this.cacheStoreData(store);
				return;
			}

			this.cacheStoreData(store);

			var el = Ext.get(this.dataviewID + "-" + calcItem.internalId);

			// if there is nothing rendered, force a refresh and return. This
			// happens when loading asynchronously (was not
			// covered correctly in previous versions, which only accepted local
			// data)
			if (!el) {
				dataview.refresh();
				return true;
			}

			// calculate the number of rows and columns we have
			var itemWidth = el.getMargin('lr') + el.getWidth(), itemHeight = el
					.getMargin('bt')
					+ el.getHeight(), dvWidth = parentEl.dom.clientWidth, columns = Math
					.floor(dvWidth / itemWidth), rtl = this.dataview
					.getHierarchyState().rtl, styleSide = rtl
					? 'right'
					: 'left', newStyle;

			// stores the current top and left values for each element
			// (discovered below)
			var oldPositions = {}, newPositions = {}, elCache = {};

			// find current positions of each element and save a reference in
			// the elCache
			Ext.iterate(previous, function(id, item) {
						var id = item.internalId, el = elCache[id] = Ext
								.get(this.dataviewID + '-' + id);

						oldPositions[id] = {
							top : el.getY() - parentEl.getY()
									- el.getMargin('t')
									- parentEl.getPadding('t')
						};
						oldPositions[id][styleSide] = this.getItemX(el);
					}, this);

			// set absolute positioning on all DataView items. We need to set
			// position, left and
			// top at the same time to avoid any flickering
			Ext.iterate(previous, function(id, item) {
						var oldPos = oldPositions[id], el = elCache[id];

						if (el.getStyle('position') != 'absolute') {
							newStyle = {
								position : 'absolute',
								top : oldPos.top + "px"
							};
							newStyle[styleSide] = oldPos[styleSide] + "px";
							elCache[id].applyStyles(newStyle);
						}
					});

			// get new positions
			var index = 0;
			Ext.iterate(store.data.items, function(item) {
				var id = item.internalId, column = index % columns, row = Math
						.floor(index / columns), top = row * itemHeight, left = column
						* itemWidth;

				newPositions[id] = {
					top : top
				};
				newPositions[id][styleSide] = left;

				index++;
			}, this);

			// do the movements
			var startTime = new Date(), duration = this.duration, dataviewID = this.dataviewID;

			var doAnimate = function() {
				var elapsed = new Date() - startTime, fraction = elapsed
						/ duration, id;

				if (fraction >= 1) {
					for (id in newPositions) {
						newStyle = {
							top : newPositions[id].top + "px"
						};
						newStyle[styleSide] = newPositions[id][styleSide]
								+ "px";

						Ext.fly(dataviewID + '-' + id).applyStyles(newStyle);
					}

					Ext.TaskManager.stop(task);
				} else {
					// move each item
					for (id in newPositions) {
						if (!previous[id]) {
							continue;
						}

						var oldPos = oldPositions[id], newPos = newPositions[id], oldTop = oldPos.top, newTop = newPos.top, oldLeft = oldPos[styleSide], newLeft = newPos[styleSide], diffTop = fraction
								* Math.abs(oldTop - newTop), diffLeft = fraction
								* Math.abs(oldLeft - newLeft), midTop = oldTop > newTop
								? oldTop - diffTop
								: oldTop + diffTop, midLeft = oldLeft > newLeft
								? oldLeft - diffLeft
								: oldLeft + diffLeft;

						newStyle = {
							top : midTop + "px"
						};
						newStyle[styleSide] = midLeft + "px";
						Ext.fly(dataviewID + '-' + id).applyStyles(newStyle)
								.setDisplayed(true);
					}
				}
			};

			var task = {
				run : doAnimate,
				interval : 20,
				scope : this
			};

			Ext.TaskManager.start(task);

			// show new items
			Ext.iterate(added, function(id, item) {
						newStyle = {
							top : newPositions[item.internalId].top + "px"
						};
						newStyle[styleSide] = newPositions[item.internalId][styleSide]
								+ "px";
						Ext.fly(this.dataviewID + '-' + item.internalId)
								.applyStyles(newStyle).setDisplayed(true);

						Ext.fly(this.dataviewID + '-' + item.internalId)
								.animate({
											remove : false,
											duration : duration,
											opacity : 1
										});
					}, this);

			this.cacheStoreData(store);
		}
	},

	getItemX : function(el) {
		var rtl = this.dataview.getHierarchyState().rtl, parentEl = el.up('');

		if (rtl) {
			return parentEl.getViewRegion().right - el.getRegion().right
					+ el.getMargin('r');
		} else {
			return el.getX() - parentEl.getX() - el.getMargin('l')
					- parentEl.getPadding('l');
		}
	},

	/**
	 * Caches the records from a store locally for comparison later
	 * 
	 * @param {Ext.data.Store}
	 *            store The store to cache data from
	 */
	cacheStoreData : function(store) {
		this.cachedStoreData = {};

		store.each(function(record) {
					this.cachedStoreData[record.internalId] = record;
				}, this);
	},

	/**
	 * Returns all records that were already in the DataView
	 * 
	 * @return {Object} All existing records
	 */
	getExisting : function() {
		return this.cachedStoreData;
	},

	/**
	 * Returns the total number of items that are currently visible in the
	 * DataView
	 * 
	 * @return {Number} The number of existing items
	 */
	getExistingCount : function() {
		var count = 0, items = this.getExisting();

		for (var k in items) {
			count++;
		}

		return count;
	},

	/**
	 * Returns all records in the given store that were not already present
	 * 
	 * @param {Ext.data.Store}
	 *            store The updated store instance
	 * @return {Object} Object of records not already present in the dataview in
	 *         format {id: record}
	 */
	getAdded : function(store) {
		var added = {};

		store.each(function(record) {
					if (this.cachedStoreData[record.internalId] == undefined) {
						added[record.internalId] = record;
					}
				}, this);

		return added;
	},

	/**
	 * Returns all records that are present in the DataView but not the new
	 * store
	 * 
	 * @param {Ext.data.Store}
	 *            store The updated store instance
	 * @return {Array} Array of records that used to be present
	 */
	getRemoved : function(store) {
		var removed = [], id;

		for (id in this.cachedStoreData) {
			if (store.findBy(function(record) {
						return record.internalId == id;
					}) == -1) {
				removed.push(this.cachedStoreData[id]);
			}
		}

		return removed;
	},

	/**
	 * Returns all records that are already present and are still present in the
	 * new store
	 * 
	 * @param {Ext.data.Store}
	 *            store The updated store instance
	 * @return {Object} Object of records that are still present from last time
	 *         in format {id: record}
	 */
	getRemaining : function(store) {
		var remaining = {};

		store.each(function(record) {
					if (this.cachedStoreData[record.internalId] != undefined) {
						remaining[record.internalId] = record;
					}
				}, this);

		return remaining;
	}
});
